#!/usr/bin/env python
# 
# Copyright under GPLv3

## @mainpage Chinesepod API Library for Python
#    @authors Harry Eakins
#    @section Introduction
#    This library provides functions for each of the Chinesepod API's
#               methods and returns data as a python object (abstracting away
#               from the XML). It requires the installation of oauth2 and lxml
#   \n Everything in it was heavily influenced by (and is basically 
#               just a modified version of) the python twitter oauth library.
#               You can find the project at 
#               http://code.google.com/p/oauth-python-twitter/ 
#               Big thanks to Hameedullah Khan for his work
#   \n To get started checkout the documentation for the class: OAuthApi


# Library modules
import urllib2
import time
from lxml import objectify
import re

# In Python2.6 the parse_qsl() function is located in the urlparse library.  In earlier versions it was part of the cgi library.
# Use "import cgi as urlparse" for Python2.5 and earlier, use "import urlparse" for Python2.6
# Swapping these two lines is all that is required to make the code compatible with the other version

#import cgi as urlparse
import urlparse

# Non library modules
import oauth2 as oauth

# Taken from oauth implementation at: http://github.com/harperreed/twitteroauth-python/tree/master
REQUEST_TOKEN_URL = 'http://chinesepod.com/oauth/request_token'
ACCESS_TOKEN_URL = 'http://chinesepod.com/oauth/access_token'
AUTHORIZATION_URL = 'http://chinesepod.com/oauth/authorize'


class OAuthApi:
    def __init__(self, consumer_key, consumer_secret, token=None, token_secret=None):
    	if token and token_secret:
    		token = oauth.Token(token, token_secret)
    	else:
    		 token = None
        self._Consumer = oauth.Consumer(consumer_key, consumer_secret)
        self._signature_method = oauth.SignatureMethod_HMAC_SHA1()
        self._access_token = token 

    def _GetOpener(self):
        opener = urllib2.build_opener()
        return opener
        
    def _FetchUrl(self,
                    url,
                    http_method=None,
                    parameters=None):
        '''Fetch a URL, optionally caching for a specified time.
    
        Args:
          url: The URL to retrieve
          http_method: 
          	One of "GET" or "POST" to state which kind 
          	of http call is being made
          parameters:
            A dict whose key/value pairs should encoded and added 
            to the query string, or generated into post data. [OPTIONAL]
            depending on the http_method parameter
    
        Returns:
          A string containing the body of the response.
        '''
        # Build the extra parameters dict
        extra_params = {}
        if parameters:
          extra_params.update(parameters)
        
        req = self._makeOAuthRequest(url, params=extra_params, 
                                                    http_method=http_method)
        
        # Get a url opener that can handle Oauth basic auth
        opener = self._GetOpener()

        if http_method == "POST":
            encoded_post_data = req.to_postdata()
            # Removed the following line due to the fact that OAuth2 request objects do not have this function
            # This does not appear to have any adverse impact on the operation of the toolset
            #url = req.get_normalized_http_url()
        else:
            url = req.to_url()
            encoded_post_data = ""
            
        if encoded_post_data:
        	url_data = opener.open(url, encoded_post_data).read()
        else:
        	url_data = opener.open(url).read()
        opener.close()
    
        # Always return the latest version
        return url_data
    
    def _makeOAuthRequest(self, url, token=None,
                                        params=None, http_method="GET"):
        '''Make a OAuth request from url and parameters
        
        Args:
          url: The Url to use for creating OAuth Request
          parameters:
             The URL parameters
          http_method:
             The HTTP method to use
        Returns:
          A OAauthRequest object
        '''
        
        oauth_base_params = {
        'oauth_version': "1.0",
        'oauth_nonce': oauth.generate_nonce(),
        'oauth_timestamp': int(time.time())
        }
        
        if params:
            params.update(oauth_base_params)
        else:
            params = oauth_base_params
        
        if not token:
            token = self._access_token
        request = oauth.Request(method=http_method,url=url,parameters=params)
        request.sign_request(self._signature_method, self._Consumer, token)
        return request

    def getAuthorizationURL(self, token, url=AUTHORIZATION_URL):
        '''Create a signed authorization URL
        
        Authorization provides the user with a VERIFIER which they may in turn provide to
        the consumer.  This key authorizes access.  Used primarily for clients.
        
        Returns:
          A signed OAuthRequest authorization URL 
        '''
        return "%s?oauth_token=%s" % (url, token['oauth_token'])

    def getRequestToken(self, url=REQUEST_TOKEN_URL):
        '''Get a Request Token from Chinesepod
        
        Returns:
          A OAuthToken object containing a request token
        '''
        resp, content = oauth.Client(self._Consumer).request(url, "GET")
        if resp['status'] != '200':
            raise Exception("Invalid response %s." % resp['status'])

        return dict(urlparse.parse_qsl(content))
    
    def getAccessToken(self, token, verifier=None, url=ACCESS_TOKEN_URL):
        '''Get a Request Token from Chinesepod
        
        Note: Verifier is required if you AUTHORIZED, it can be skipped if you AUTHENTICATED
        
        Returns:
          A OAuthToken object containing a request token
        '''
        token = oauth.Token(token['oauth_token'], token['oauth_token_secret'])
        if verifier:
            token.set_verifier(verifier)
        client = oauth.Client(self._Consumer, token)
        
        resp, content = client.request(url, "POST")
        return dict(urlparse.parse_qsl(content))
    
    ## Returns detailed information about the user's account and settings
    #  @param None
    #  @return an object containing the users information. E.g.
    #  @verbatim user = None [ObjectifiedElement]
    #   user_id = 12345 [IntElement]
    #   email = 'example@abc.com' [StringElement]
    #   username = 'JohnSmith' [StringElement]
    #   user_avatar = 'http://chinesepod.com/data/avatar/default.jpg' [StringElement]
    #   account_type_name = 'basic' [StringElement]
    #   expired_at = '2010-09-03T11:45:08-07:00' [StringElement]
    #   country = u'' [StringElement]
    #   province = u'' [StringElement]
    #   city = u'' [StringElement]
    #  @endverbatim 
    #
    #  More info can be found here http://apiwiki.chinesepod.com/index.php?title=Cpodapi/user_settings
    def GetUserSettings(self, options={}):
    	return self.ApiCall("user_settings", "GET", options)
   
    ## Retrieves information about another user such as username, email, avatar, country, etc...
    #  @param user_id The user_id number of the user
    def GetUserProfile(self, user_id, options={}):
        options['user_id'] = user_id
    	return self.ApiCall("view_profile", "GET", options)
   
    ## Returns a list of groups
    #  @param group_type Optional. Default: Channel
    #  @param offset Optional.Default: 0
    #  @param limit Optional. Default: 10. Max: 20
    #  
    #  More info at http://apiwiki.chinesepod.com/index.php?title=Cpodapi/groups_list
    def GetGroups(self, group_type = 'Channel', offset = 0, limit=10, options={}):
        options['group_type'] = group_type
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("groups_list", "GET", options)
   
    ## Retrieve a list of groups you are subscribed to.
    #  @param offset Optional.Default: 0
    #  @param limit Optional. Default: 10. Max: 20
    #  
    #  More info at http://apiwiki.chinesepod.com/index.php?title=Cpodapi/my_groups_list
    def GetMyGroups(self, offset = 0, limit = 10, options={}):
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("my_groups_list", "GET", options)

    ## Join a group
    #  @param group_id The number identifying the group to join
    def JoinGroup(self, group_id, options={}):
        options['group_id'] = group_id
    	return self.ApiCall("join_group", "POST", options)

    ## Leave a group
    #  @param group_id The number identifying the group to leave
    def LeaveGroup(self, group_id, options={}):
        options['group_id'] = group_id
    	return self.ApiCall("leave_group", "POST", options)

    ## Add a post to a group
    #  @param title The title of the post to be added
    #  @param content The content of the post
    #  @group_id The number identifying the group being posted to
    def AddGroupPost(self, title, content, group_id, options={}):
        options['title'] = title
        options['content'] = content
        options['group_id'] = group_id
    	return self.ApiCall("group_post_add", "POST", options)

    ## Comment on a group post
    #  @param content The content of the post comment.
    #  @param post_id The id number of the post to be commmented on.
    #  @param reply_to_id The id of the comment being commented on. Optional.
    #  @param reply_to_id2 The id of the comment-on-a-comment being commented on. Optional.
    #  @param reply_to_user_id The id of the user whose post is being replied to. Optional.
    #
    #  This cannot be accessed by free users.
    def AddCommentToGroupPost(self, content, post_id, reply_to_id = 0, 
                        reply_to_id2 = 0, reply_to_user_id = 0, options={}):
        options['content'] = content
        options['post_id'] = post_id
        options['reply_to_id'] = reply_to_id
        options['reply_to_id2'] = reply_to_id2
        options['reply_to_user_id'] = reply_to_user_id
    	return self.ApiCall("group_post_comment_add", "POST", options)

    ## List the posts for a certain group
    #  @param group_id The ID number of the group
    #  @param offset Optional.
    #  @param limit. Optional. Max. 20
    def GetGroupPosts(self, group_id, offset = 0, limit = 10, options={}):
        options['group_id'] = group_id
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("group_posts_list", "GET", options)

    ## Retrieve a list of the comments on a particular post
    #  @param post_id The ID of the post.
    #  @param offset Optional.
    #  @param limit Optional. Max 20.
    def GetGroupPostComments(self, post_id, offset = 0, limit = 10, options={}):
        options['post_id'] = post_id
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("group_post_comments_list", "GET", options)

    ## Get a list of the latest lessons
    #  @param offset Optional.
    #  @param limit. Optional. Max. 20
    def GetLatestLessons(self, offset = 0, limit = 10, options={}):
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("latest_lessons_list", "GET", options)

    ## Get a list of the latest lessons which you have bookmarked
    #  @param group_id A number which is unique to each group (i.e. channel) 
    #                  e.g. Shanghainese is 969. Optional.
    #  @param status Optional.
    #  @param offset Optional.
    #  @param limit. Optional. Max. 20
    def GetMyLessons(self, group_id = '', status = '', offset = 0, limit = 10, options={}):
        options['group_id'] = group_id
        options['status'] = status
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("my_lessons_list", "GET", options)

    ## Gets information about a lesson including the URLs for downloading media, dialogs and
    #  vocabulary lists
    # @param v3_id The v3_id code of the lesson you want to retrieve
    # 
    # Free users cannot access this method.
    # Basic users can access it, but will be unable to retrieve dialogues/expansions/vocabulary
    def GetLesson(self, v3_id, options={}):
        options['v3_id'] = v3_id
    	return self.apicall("lesson_retrieve", "GET", options)

    ## Sets the lesson as bookmarked 
    #  @param v3_id The v3_id code of the lesson you want to bookmark
    def SetLessonBookmarked(self, v3_id,  options={}):
        options['v3_id'] = v3_id
    	return self.ApiCall("lesson_set_bookmarked", "POST", options)

    ## Sets the lesson as studied 
    #  @param v3_id The v3_id code of the lesson to be set to 'studied'
    def SetLessonStudied(self, v3_id,  options={}):
        options['v3_id'] = v3_id
    	return self.ApiCall("lesson_set_studied", "POST", options)

    ## Remove a lesson from your studying list
    #  @param v3_id The v3_id code of the lesson to be removed
    #
    #  More info http://apiwiki.chinesepod.com/index.php?title=Cpodapi/lesson_set_removed
    def RemoveLesson(self, v3_id, options={}):
        options['v3_id'] = v3_id
    	return self.ApiCall("lesson_set_removed", "POST", options)

    ## Get the list of comments for a particular lesson
    #  @param v3_id The v3_id code of the lesson to retrieve comments from
    #  @param offset Optional.
    #  @param limit. Optional. Max. 20
    def GetLessonComments(self, v3_id, offset = 0, 
                                limit = 10, options={}):
        options['v3_id'] = v3_id
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("lesson_comments_list", "GET", options)

    ## Post a comment on a lesson
    #  @param content Content of the comment
    #  @param v3_id The v3_id code of the lesson you want to comment on
    #  @param reply_to_id The comment_id of the comment being replied to. Optional.
    #  @param reply_to_id2 The comment_id of the comment-on-a-comment being replied to. Optional.
    #  @param reply_to_user_id The user_id of the person being replied to. Optional
    #  @param group_id ID of the group for this lesson
    #  
    #  If you are replying to a reply, you need to include both reply_to_id and reply_to_id2 
    #  where the former is the original comment's comment_id and the latter is the comment_id
    #  of the reply you are replying to.
    #  More info at http://apiwiki.chinesepod.com/index.php?title=Cpodapi/lesson_comment_add
    def AddLessonComment(self, content, v3_id, group_id, reply_to_id = 0, 
                                reply_to_id2 = 0, reply_to_user_id = 0,
                                    options={}):
        options['content'] = content
        options['v3_id'] = v3_id
        options['reply_to_id'] = reply_to_id
        options['reply_to_id2'] = reply_to_id2
        options['reply_to_user_id'] = reply_to_user_id
        options['group_id'] = group_id
    	return self.ApiCall("lesson_comment_add", "POST", options)

    ## Returns a list of the the users conversations
    #  @param group_id Restrict the conversations to a specific group with this. Optional.
    #  @param type Optional.
    #  @param offset Optional.
    #  @param limit Optional. Max: 20
    def GetMyConversations(self, group_id = '', type = '',
                            offset = 0, limit = 10, options={}):
        options['group_id'] = group_id
        options['type'] = type
        options['offset'] = offset
        options['limit'] = limit
    	return self.ApiCall("my_conversations", "GET", options)

    ## Retrieve a list of words from the user's flashcard set
    #
    #  Free and basic users cannot access this method
    def GetWords(self, options={}):
    	return self.ApiCall("words_list", "GET", options)

    ## Adds a word to the user's flashcard set
    # @param source
    # @param target
    # @param extra1 Optional.
    # @param extra2 Optional.
    # @param audio Optional. 
    #
    # More info at http://apiwiki.chinesepod.com/index.php?title=Cpodapi/word_add
    def AddWord(self, source, target, extra1 = '', extra2 = '', audio = '', options={}):
        options['source'] = source
        options['target'] = target
        options['extra1'] = extra1
        options['extra2'] = extra2
        options['audio'] = audio
    	return self.ApiCall("word_add", "POST", options)

    ## Delete a word from the user's flashcard set
    #  @param word_id The ID of the word to be deleted
    #
    #  Free and basic users cannot access this method
    def DeleteWord(self, word_id, options={}):
        options['word_id'] = word_id
    	return self.ApiCall("word_delete", "POST", options)

    ## Calls the Chinesepod API
    #  @param call The name of the api call
    #  @param type One of either "GET" or "POST"
    #  @param parameters Parameters to pass to the Chinesepod API call
    #  @return the lxml.objectify object
    def ApiCall(self, call, type="GET", parameters={}):
        return_value = []
          # We use this try block to make the request in case we run into one of  503 (temporarily unavailable) errors.
          # Other error handling may end up being useful as well.
        try:
            xml = self._FetchUrl("http://chinesepod.com/cpodapi/" + call + "/xml", type, parameters)
        except urllib2.HTTPError, e:
            return e
          # Getting an URLError usually means you didn't even hit Chinesepod's servers. This means something has gone TERRIBLY WRONG somewhere.
        except urllib2.URLError, e:
            return e
        else:
            xml = re.sub("& ", "&amp; ", xml) # Because some Chinesepod xml is badly formed
            return_value = objectify.fromstring(xml)
            if hasattr(return_value, "Errors"):
                raise Exception(return_value.Errors.ErrorCode,
                                    return_value.Errors.ErrorType,
                                    return_value.Errors.ErrorMessage)
                                    
            return return_value

